iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
SideProject30

30 天用 Rust 打造 QR Code 製造機系列 第 24

Day 24 - 簡易 QR code 表單元件

  • 分享至 

  • xImage
  •  

之前在開發 API 的時候,有寫一隻比較簡單的建立 QR code 的 API,但是後來都沒用到,所以這篇文章會來建立一個表單元件來使用這隻 API。

建立表單元件

其實這個表單跟之前建立 SVG 的表單很像,只是差別在於沒有一些細部調整的功能,所以樣式的部分,大部分都可以拿之前的表單來使用。

import { useEffect } from 'react'
import { useForm, SubmitHandler } from 'react-hook-form'
import generateQrcode from '../lib/api/generateQrcode'
import { TextInput, SelectType } from './'
import useStore from '../store'

// 型別只要這個表單需要的就好
export type PngFormInputs = {
  text: string
  qrType: string
}

// 型別只要這個表單需要的就好
type QrCodeData = {
  url?: string
  phone?: string
  address?: string
  email?: string
}

const CreatePngForm = () => {
  const { setImgSrc, setErrors } = useStore()
  const {
    register,
    handleSubmit,
    formState: { errors },
    watch
  } = useForm<PngFormInputs>()

  const qrType = watch('qrType', 'URL')

  useEffect(() => {
    setImgSrc(null)
  }, [qrType])

  useEffect(() => {
    setErrors(errors)
  }, [errors])

  const validationPatterns = {
    URL: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
    電話: /^(\+?\d{1,3}[-.\s]?)?\d{10}$/,
    地址: /.+/,
    Email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  }

  const pattern = validationPatterns[qrType as keyof typeof validationPatterns]

  const fetchQrcodePng = async (formData: PngFormInputs) => {
    try {
      const typeMapping: {
        [key in PngFormInputs['qrType']]: keyof QrCodeData
      } = {
        URL: 'url',
        電話: 'phone',
        地址: 'address',
        Email: 'email'
      }

      const dataKey = typeMapping[formData.qrType]
      const data: QrCodeData = {
        [dataKey]: formData.text
      }
      const response = await generateQrcode.getPng(data)
      const blob = new Blob([response.data], { type: 'image/png+xml' })
      const objectURL = URL.createObjectURL(blob)
      setImgSrc(objectURL)
    } catch (_) {
      console.error('Error fetching image')
    }
  }

  const onSubmit: SubmitHandler<PngFormInputs> = async (data) => {
    await fetchQrcodePng(data)
  }

  const hasErrors = Object.keys(errors).length > 0

  return (
    <form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-y-5'>
      <SelectType register={register} />
      <TextInput register={register} pattern={pattern} errors={errors} />
      <div className='flex justify-center mt-5'>
        <button
          type='submit'
          className={`${
            hasErrors ? 'bg-gray-400' : 'bg-green-500'
          } hover:bg-green-100 text-white hover:text-slate-700 font-bold py-2 px-4 rounded text-center`}
        >
          產生 QR Code
        </button>
      </div>
    </form>
  )
}

export default CreatePngForm

form 表單的部分,保留 SelectTypeTextInput,然後將 onSubmit 改為使用 fetchQrcodePng 來取得 QR code 圖片,最後將圖片的 URL 設定到狀態中。記得要把 Blob 的型別設定為 image/png+xml,這樣才能正確顯示圖片。

在型別的部分只要這個表單需要的就好,所以需要修改一下,並且更改一下 type 名稱,等一下比較好辨識。

SelectType 和 TextInput 元件型別修正

現在應該可以看到 CreatePngForm<SelectType /><TextInput /> 有紅色的底線,這是因為型別不正確,所以我們要修正一下。

既然這兩個元件是在兩邊表單都會使用到,不過兩邊的型別不太一樣,而且最大的差異是在一個是 SVG,一個是 PNG,所以就可以思考一下,要怎麼讓這兩個元件可以共用,但是又可以有不同的型別。

解決的方法有很多,這邊選擇傳入一個 props,讓元件可以根據 props 來決定型別。

SelectType 的型別修正:

import { FC } from 'react'
import { UseFormRegister } from 'react-hook-form'
import { SvgFormInputs } from './CreateSvgForm'
import { PngFormInputs } from './CreatePngForm'

type ImageType = 'SVG' | 'PNG'

interface SelectTypeProps<T extends ImageType> {
  register: T extends 'SVG'
    ? UseFormRegister<SvgFormInputs>
    : UseFormRegister<PngFormInputs>
  imageType: T
}

const SelectType: FC<SelectTypeProps<ImageType>> = ({
  register,
  imageType
}) => {
  let adjustedRegister: any

  if (imageType === 'SVG') {
    adjustedRegister = register as UseFormRegister<SvgFormInputs>
  } else {
    adjustedRegister = register as UseFormRegister<PngFormInputs>
  }

  return (
    <div className='mb-4'>
      <label className='block font-bold mb-2'>選擇 QR Code 類型</label>
      <select
        className='w-full p-2 border rounded focus:outline-none focus:ring focus:border-blue-300'
        {...adjustedRegister('qrType')}
      >
        <option value='URL'>URL</option>
        <option value='電話'>電話</option>
        <option value='地址'>地址</option>
        <option value='Email'>Email</option>
      </select>
    </div>
  )
}

TextInput 的型別修正:

import { UseFormRegister, FieldErrors } from 'react-hook-form'
import { SvgFormInputs } from './CreateSvgForm'
import { PngFormInputs } from './CreatePngForm'

type ImageType = 'SVG' | 'PNG'

interface TextInputProps<T extends ImageType> {
  register: T extends 'SVG'
    ? UseFormRegister<SvgFormInputs>
    : UseFormRegister<PngFormInputs>
  pattern: RegExp
  errors: FieldErrors<SvgFormInputs>
  imageType: T
}

const TextInput: FC<TextInputProps<ImageType>> = ({
  register,
  pattern,
  errors,
  imageType
}) => {
  let adjustedRegister: any

  if (imageType === 'SVG') {
    adjustedRegister = register as UseFormRegister<SvgFormInputs>
  } else {
    adjustedRegister = register as UseFormRegister<PngFormInputs>
  }

  return (
    <div className='mb-4'>
      <label className='block font-bold mb-2'>輸入文字</label>
      <input
        type='text'
        className={`w-full p-2 border rounded focus:outline-none focus:ring ${
          errors?.text
            ? 'focus:border-red-500 border-red-500 ring-red-500'
            : 'focus:border-blue-300'
        }`}
        {...adjustedRegister('text', { required: true, pattern })}
      />
    </div>
  )
}

這兩個元件的處理方式都差不多,都是使用 Type Guard 來處理,這樣就可以讓元件可以根據 imageType 來決定要使用哪個型別,只是在 TextInput 中,因為要使用 pattern 來驗證輸入的文字,所以也要將 pattern 傳入 props。

最後在 CreateSvgFormCreatePngForm 中,將 SelectTypeTextInput 傳入 props:

CreateSvgForm

import { useForm, SubmitHandler, UseFormRegister } from 'react-hook-form'

// 省略其他程式碼
<SelectType register={register as UseFormRegister<SvgFormInputs>} imageType="SVG" />
<TextInput register={register as UseFormRegister<SvgFormInputs>} pattern={pattern} errors={errors} imageType="SVG" />

CreatePngForm

import { useForm, SubmitHandler, UseFormRegister } from 'react-hook-form'

// 省略其他程式碼
<SelectType register={register as UseFormRegister<PngFormInputs>} imageType="PNG" />
<TextInput register={register as UseFormRegister<PngFormInputs>} pattern={pattern} errors={errors} imageType="PNG" />

最後就可以看到兩邊的表單都可以正常運作了。


上一篇
Day 23 - 運用 Next.js 的 CSS Module 功能修改樣式
下一篇
Day 25 - 動態調整 Next.js Image 大小
系列文
30 天用 Rust 打造 QR Code 製造機30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言